package fidl;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Iterator;

import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.resource.Resource;
import org.franca.core.franca.FAttribute;
import org.franca.core.franca.FBasicTypeId;
import org.franca.core.franca.FBroadcast;
import org.franca.core.franca.FEnumerationType;
import org.franca.core.franca.FEnumerator;
import org.franca.core.franca.FInterface;
import org.franca.core.franca.FModel;
import org.franca.core.franca.FModelElement;
import org.franca.core.franca.FStructType;
import org.franca.core.franca.FType;
import org.franca.core.franca.FTypeCollection;
import org.franca.core.franca.FTypeRef;

public class JsonGenerator {
	Configure configure = null;
	FModel model = null;
	FTypeCollection container = null;
	String filename = "";
	StringBuilder body = new StringBuilder();
	String indent = "";

	void increaseIndent() {
		indent += "\t";
	}

	void decreaseIndent() {
		indent = indent.substring(1);
	}

	boolean doGenerate(Resource resource, Configure configure) {
		this.configure = configure;
		this.filename = Utils.fetchFileName(resource);
		if (resource.getContents().isEmpty()) {
			System.out.println("An empty file: " + resource.getURI().path());
			return false;
		}
		EObject root = resource.getContents().get(0);
		if (root instanceof FModel) {
			model = (FModel) root;
		} else {
			System.out.println("Unsupport file: " + resource.getURI().path());
			return false;
		}
		body.append("// Generated by FrancaIDL tool, Do **NOT** Modify!\n\n"+
			"#pragma once\n\n" +
			"#include <cstdint>\n" +
			"#include <string>\n" +
			"#include <vector>\n" +
			"#include <map>\n\n" +
			"#include <json/json.h>\n\n" +
			"#include \"TypeAdaptionTraits.hpp\"\n" +
			"#include \"" + filename + ".hpp\"\n\n");
		for (FTypeCollection typeCollection : model.getTypeCollections()) {
			generate(body, typeCollection);
		}
		for (FInterface interface_ : model.getInterfaces()) {
			generate(body, interface_);
		}
		return true;
	}

	public boolean writeToFile(String directory) {
		String namespace = model.getName();
		if (!configure.PrefixNamespace.isEmpty()) {
			namespace = configure.PrefixNamespace + "." + namespace;
		}
		BufferedWriter bufferedWriter = null;
		try {
			String filename = directory + "/" + namespace.replace(".", "/") +
				"/" + this.filename + "4Json.hpp";
			File file = Utils.createFile(filename);
			bufferedWriter = new BufferedWriter(new FileWriter(file));
			bufferedWriter.write(body.toString());
			bufferedWriter.close();
		} catch (IOException e) {
			e.printStackTrace();
			return false;
		} finally {
			try {
				bufferedWriter.close();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		return true;
	}

	boolean generate(StringBuilder builder, FTypeCollection typeCollection) {
		container = typeCollection;
		for (FType type : container.getTypes()) {
			generateType(builder, type);
		}
		if (container instanceof FInterface) {
			FInterface interface_ = (FInterface) container;
			for (FAttribute attribute : interface_.getAttributes()) {
				generateAttribute(builder, attribute);
			}
			for (FBroadcast broadcast : interface_.getBroadcasts()) {
				String typeName = resovleTypeName(broadcast);
				generateJson(builder, typeName, broadcast.getOutArgs());
			}
			// for (FMethod method : interface_.getMethods()) {
			// generateMethod(builder, method);
			// }
		}
		return true;
	}

	void generateAttribute(StringBuilder builder, FAttribute attribute) {
		FTypeRef typeRef = attribute.getType();
		FType baseType = typeRef.getDerived();
		if (baseType != null &&
			baseType.eContainer() == attribute.eContainer() &&
			baseType.getName().equals(attribute.getName())) {
			// attribute is same of attribute type
			return;
		}
		String typeName = resovleTypeName(attribute);
		builder.append(
			"template<>\n" +
			"struct TypeAdaptionTraits<" + typeName + ">\n{\n" +
			"\tusing AdaptedType = " + typeName + ";\n\n" +
			"\tstatic AdaptedType getDefaultValue()\n\t{\n" +
			"\t\treturn AdaptedType{};\n\t}\n\n" +
			"\tstatic bool toJson(Json::Value &o, const AdaptedType &v)\n" +
			"\t{\n\t\treturn ::toJson(o, v.m_value);\n\t}\n\n" +
			"\tstatic bool fromJson(const Json::Value &o, AdaptedType &v)\n" +
			"\t{\n\t\treturn ::fromJson(o, v.m_value);\n\t}\n};\n\n"
		);
	}

	void generateType(StringBuilder builder, FType type) {
		if (type instanceof FStructType) {
			FStructType struct = (FStructType) type;
			String typeName = resovleTypeName(struct);
			generateJson(builder, typeName, struct.getElements());
		} else if (type instanceof FEnumerationType) {
			FEnumerationType enumeration = (FEnumerationType) type;
			String typeName = resovleTypeName(enumeration);
			generateJson(builder, typeName, enumeration.getEnumerators());
		}
	}

	<T extends FModelElement>
	void generateJson(StringBuilder builder, String typeName, EList<T> args) {
		builder.append(
			"template<>\n" +
			"struct TypeAdaptionTraits<" + typeName + ">\n{\n" +
			"\tusing AdaptedType = " + typeName + ";\n\n" +
			"\tstatic AdaptedType getDefaultValue()\n\t{\n" +
			"\t\treturn AdaptedType{};\n\t}\n\n" +
			"\tstatic bool toJson(Json::Value &o, const AdaptedType &v)\n\t{\n"
		);
		if (args.isEmpty()) { // maybe empty? I don't know, return true anyway.
			builder.append("true");
		} else {
			if (args.get(0) instanceof FEnumerator) {
				generateEnumToJson(builder, args);
			} else {
				generateFromOrTo(builder, false, args);
			}
		}
		builder.append(";\n\t}\n\n");
		builder.append("\tstatic bool fromJson(const Json::Value &o, " +
			"AdaptedType &v)\n\t{\n");
		if (args.isEmpty()) { // maybe empty? I don't know, return true anyway.
			builder.append("true");
		} else {
			if (args.get(0) instanceof FEnumerator) {
				generateEnumFromJson(builder, args);
			} else {
				generateFromOrTo(builder, true, args);
			}
		}
		builder.append(";\n\t}\n};\n\n");
	}

	<T extends FModelElement>
	void generateFromOrTo(StringBuilder builder, boolean from, EList<T> args) {
		String method = from ? "::fromJson" : "::toJson";
		builder.append("\t\treturn ");
		for (Iterator<T> itr = args.iterator(); itr.hasNext();) {
			String name = itr.next().getName();
			builder.append(method + "(o[\"" + name + "\"], " +
				"v.m_" + name + ")");
			if (itr.hasNext()) {
				builder.append(" &&\n\t\t\t");
			}
		}
	}

	<T extends FModelElement>
	void generateEnumFromJson(StringBuilder builder, EList<T> args) {
		String method = "::fromJson";
		builder.append("\t\tstd::string s;\n");
		builder.append("\t\tif (!" + method + "(o, s))\n\t\t{\n" +
			"\t\t\treturn false;\n\t\t}\n");
		for (Iterator<T> itr = args.iterator(); itr.hasNext();) {
			String name = itr.next().getName();
			builder.append("\t\tif (s == \"" + name + "\")\n\t\t{\n" +
				"\t\t\tv = AdaptedType::" + name + ";\n" +
				"\t\t\treturn true;\n\t\t}\n");
		}
		builder.append("\t\treturn false");
	}

	<T extends FModelElement>
	void generateEnumToJson(StringBuilder builder, EList<T> args) {
		String method = "::toJson";
		builder.append("\t\tstd::string s;\n");
		builder.append("\t\tswitch (v)\n\t\t{\n");
		for (Iterator<T> itr = args.iterator(); itr.hasNext();) {
			String name = itr.next().getName();
			builder.append("\t\tcase AdaptedType::" + name + ":\n" +
				"\t\t\ts = \"" + name + "\";\n\t\t\tbreak;\n");
		}
		builder.append("\t\t}\n\t\treturn " + method + "(o, s)");
	}

	String resovleTypeName(FTypeRef type) {
		FBasicTypeId id = type.getPredefined();
		switch (id.getValue()) {
		case FBasicTypeId.BOOLEAN_VALUE:
			return "bool";
		case FBasicTypeId.INT8_VALUE:
		case FBasicTypeId.INT16_VALUE:
		case FBasicTypeId.INT32_VALUE:
			return "int32_t";
		case FBasicTypeId.INT64_VALUE:
			return "int64_t";
		case FBasicTypeId.UINT8_VALUE:
		case FBasicTypeId.UINT16_VALUE:
		case FBasicTypeId.UINT32_VALUE:
			return "uint32_t";
		case FBasicTypeId.UINT64_VALUE:
			return "uint64_t";
		case FBasicTypeId.FLOAT_VALUE:
			return "float";
		case FBasicTypeId.DOUBLE_VALUE:
			return "double";
		case FBasicTypeId.STRING_VALUE:
		case FBasicTypeId.BYTE_BUFFER_VALUE:
			return "std::string";
		default:
			return resovleTypeName(type.getDerived());
		}
	}

	String resovleTypeName(FModelElement baseType) {
		if (baseType == null) {
			return "undefined";
		}
		String fullName = baseType.getName();
		while (true) {
			EObject object = baseType.eContainer();
			if (object instanceof FModelElement) {
				baseType = (FModelElement) object;
				String typeName = baseType.getName();
				if (typeName != null) {
					fullName = typeName + "." + fullName;
				}
			} else if (object instanceof FModel) {
				FModel containerModel = (FModel) object;
				fullName = containerModel.getName() + "." + fullName;
				break;
			} else {
				return "undefined";
			}
		}
		fullName = fullName.replaceAll("\\.", "::");
		return fullName;
	}
}
